/*
 * Copyright 2008-2009 Adam Tacy <adam.tacy AT gmail.com>
 * 
 * Licensed under the Apache License, Version 2.0 (the "License"); you may not
 * use this file except in compliance with the License. You may obtain a copy of
 * the License at
 * 
 * http://www.apache.org/licenses/LICENSE-2.0
 * 
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
 * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
 * License for the specific language governing permissions and limitations under
 * the License.
 */
package org.adamtacy.client.ui.effects.core;

import java.util.HashMap;
import java.util.Iterator;
import java.util.Vector;

import org.adamtacy.client.ui.effects.Effect;
import org.adamtacy.client.ui.effects.events.EffectSteppingEvent;
import org.adamtacy.client.ui.effects.impl.css.StyleImplementation;
import org.adamtacy.client.ui.effects.impl.css.Rule;
import org.adamtacy.client.ui.effects.impl.css.Property;
import org.adamtacy.client.ui.effects.impl.css.Selector;

import com.google.gwt.core.client.GWT;
import com.google.gwt.dom.client.Element;

/**
 * Class that represents a morphing between two styles, e.g.
 * 
 * start {top: 0px; left: 0px;} -> end {top: 100px; left: 100px;}
 * 
 * 
 * @author adam
 * 
 */
public class NMorphStyle extends Effect {

	/**
	 * Inner class representing a pair of properties start can be null, but the
	 * end should not be
	 * 
	 * @author adam
	 * 
	 */
	protected class PropertyPair {
		protected Property start, end;

		public PropertyPair(Property start, Property end) {
			this.start = start;
			this.end = end;
			if (end == null)
				throw new RuntimeException(
						"End value of a property pair shold not be null");
		}

		public Property getEnd() {
			return end;
		}

		public Property getStart() {
			return start;
		}
	}

	/**
	 * Set of effects that it has been determined make up the morph from the
	 * start to the end styles supplied in the constructor.
	 */
	//protected HashMap<String, ChangeInterface> determinedEffects;

	/**
	 * Set of effects for multiple elements.
	 * 
	 */
	protected HashMap<Element, HashMap<String, ChangeInterface>> elementEffects;

	/**
	 * Set of property pairs representing the valid start/end pairs of
	 * properties the effect will work over.
	 */
	protected Vector<PropertyPair> propertyPairs;

	/**
	 * Position at which all binary effects will switch state.
	 */
	protected double switchFrameNumber = 0.5;

	/**
	 * The start and end CSS rules
	 */
	protected Rule theStart, theEnd;

	/**
	 * Simple constructor
	 */
	public NMorphStyle() {
	}

	/**
	 * Construct effect to be applied to a specific DOM element
	 * 
	 * @param el
	 */
	public NMorphStyle(Element el) {
		addEffectElement(el);
	}

	/**
	 * Constructor to morph a single Property between two values
	 * 
	 * @param start
	 *            Initial property.
	 * @param end
	 *            Expected end property.
	 */
	public NMorphStyle(Property start, Property end) {
		if (!start.getName().equals(end.getName()))
			throw new RuntimeException(
					"Start and End properties must be of the same type!  You used start="
							+ start.getName() + " and end=" + end.getName());
		// Added variable initialiser - see Issue 110.
		propertyPairs = new Vector<PropertyPair>();
		propertyPairs.add(new PropertyPair(start, end));
		// Updated to store the property as a rule, see Issue 115
		theStart = new Rule("s{" + start + "}");
		theEnd = new Rule("s{" + end + "}");
	}

	/**
	 * Constructor to morph between two style rules
	 * 
	 * @param start
	 *            Initial style rule.
	 * @param end
	 *            Expected end style rule.
	 */
	public NMorphStyle(Rule start, Rule end) {
		theStart = start;
		theEnd = end;
		registerProperties(start, end, null, null);
	}

	public NMorphStyle(Element el, Rule start, Rule end) {
		addEffectElement(el);
		theStart = start;
		theEnd = end;
		registerProperties(start, end, null, null);
	}

	/**
	 * Constructor to morph between two style rules defined in an external style
	 * sheet.
	 * 
	 * @param start
	 *            Initial style rule selector.
	 * @param end
	 *            Expected end style rule selector.
	 */
	public NMorphStyle(final Selector start, final Selector end) {
		theStart = start.getRuleDefinition();
		theEnd = end.getRuleDefinition();
		registerProperties(theStart, theEnd, null, null);
	}

	public NMorphStyle(Element el, final Selector start, final Rule end) {
		addEffectElement(el);
		theStart = start.getRuleDefinition();
		theEnd = end;
		registerProperties(theStart, theEnd, null, null);
	}

	public NMorphStyle(Element el, final Rule start, final Selector end) {
		addEffectElement(el);
		theStart = start;
		theEnd = end.getRuleDefinition();
		registerProperties(theStart, theEnd, null, null);
	}

	/**
	 * Get the position at which binary effecys will switch status.
	 * 
	 * @return
	 */
	public double getSwitchFrameNumber() {
		return switchFrameNumber;
	}

	/**
	 * Update the effect by updating all the effects held in the
	 * determinedEffects HashMap
	 */
	@Override
	protected void onUpdate(double progress) {
		StringBuffer sb = new StringBuffer();

		super.onUpdate(progress);
		// For each Element....
		for (Iterator<Element> iter = effectElements.iterator(); iter.hasNext();) {
			Element e = iter.next();
			// For each determined effect
			HashMap<String, ChangeInterface> determinedEffects = elementEffects.get(e);
			for (Iterator<String> it = determinedEffects.keySet().iterator(); it
					.hasNext();) {
				String theChange = it.next();
				ChangeInterface currChange = determinedEffects.get(theChange);
				sb.append(currChange.performStep(e, theChange, progress));
				sb.append(";");
			}
		}
		fireEvent(new EffectSteppingEvent(progress, sb.toString()));
	}

	/**
	 * Creates a list of properties that we are interested in mapping between.
	 * In the worst case, this is all properties provided in the rules, however
	 * there are two spcial cases:
	 * 
	 * 1. Items that appear in the start rule but not in the end rule. The
	 * understanding here is that this property will not change over the
	 * progression, so we can ignore it. 2. Items that appear in the end rule
	 * but not in the start rule. The understanding here is that we need to
	 * progress from some as yet undefined state to the end state. When the
	 * effect is set up, an attempt is made to get the computed style for the
	 * start state (failing if the start style is determine to be "auto" since
	 * how is that to be interpreted.
	 * 
	 * @param start
	 * @param end
	 */
	protected void registerProperties(Rule start, Rule end,
			Property replaceStart, Property replaceEnd) {
		propertyPairs = new Vector<PropertyPair>();
		Property entryStart;
		Property entryEnd;
		boolean replacedStart = false;
		boolean replacedEnd = false;
		// Since we are interested in end properties, iterate over them.
		for (Iterator<String> it = end.getProperties().keySet().iterator(); it
				.hasNext();) {
			// Get the property name
			String name = it.next();
			// See if we are trying to replace this from a
			// setNew[Start|End]Property call
			if ((replaceStart != null) && (replaceStart.getName().equals(name))) {
				entryStart = replaceStart;
				replacedStart = true;
			}
			// Find the entry in the start rule - this could be null.
			else
				entryStart = start.getProperties().get(name);
			// See if we are trying to replace this from a
			// setNew[Start|End]Property call
			if ((replaceEnd != null) && (replaceEnd.getName().equals(name))) {
				entryEnd = replaceEnd;
				replacedEnd = true;
			}
			// Find the entry in the end rule - this can not be null.
			else
				entryEnd = end.getProperties().get(name);
			// Create and record a property pair
			propertyPairs.add(new PropertyPair(entryStart, entryEnd));
			// Remove the "name" key from list of start elements
			start.getProperties().remove(name);
		}
		// Now look at the remaining start properties and add them with no
		// change to
		// be made
		for (Iterator<String> it = start.getProperties().keySet().iterator(); it
				.hasNext();) {
			// Get the property name
			String name = it.next();
			// See if we are trying to replace this from a
			// setNew[Start|End]Property call
			if ((replaceStart != null) && (replaceStart.getName().equals(name))) {
				entryStart = replaceStart;
				replacedStart = true;
			}
			// Find the entry in the start rule - this cannot be null (otherwise
			// it would still be in the list....).
			else
				entryStart = start.getProperties().get(name);
			// Do same for the end property - if in replaceEnd, replace it,
			// otherwise set it to the entryStart value.
			if ((replaceEnd != null) && (replaceEnd.getName().equals(name))) {
				entryEnd = replaceEnd;
				replacedEnd = true;
			} else
				entryEnd = entryStart;
			// At this point if replacedStart or replacedEnd are still false and
			// replaceStart or replaceEnd are not null then we need to add
			if ((replaceEnd != null) && (!replacedEnd)) {
				entryStart = null;
				entryEnd = replaceEnd;
				theEnd.addProperty(replaceEnd);
			}
			if ((replaceStart != null) && (!replacedStart)) {
				entryStart = replaceStart;
				entryEnd = replaceEnd;
				theEnd.addProperty(replaceEnd);
				theStart.addProperty(replaceStart);
			}
			propertyPairs.add(new PropertyPair(entryStart, entryEnd));
		}
		GWT.log("NMorphStyle PropertyPairs: "+propertyPairs.toString());
	}

	/**
	 * Set the new end style for the effect, and if the effectElement is not
	 * null, set up the effect again.
	 * 
	 * @param end
	 *            New CSS Rule for end of the effect.
	 */
	public void setNewEndStyle(Rule end) {
		registerProperties(theStart, end, null, null);
		theEnd = end;
		if (!effectElements.isEmpty())
			setUpEffect();
	}

	/**
	 * Set the new end Property for the effect, and if the effectElement is not
	 * null, set up the effect again.
	 * 
	 * If endProp already exists in the registered end rule for this effect,
	 * then it replaces the original value, otherwise it is added as a new
	 * property.
	 * 
	 * @param end
	 *            New CSS Rule for end of the effect.
	 */
	public void setNewEndProperty(Property endProp) {
		registerProperties(theStart, theEnd, null, endProp);
		if (!effectElements.isEmpty())
			setUpEffect();
	}

	/**
	 * Set the new start Property for the effect, and if the effectElement is
	 * not null, set up the effect again.
	 * 
	 * If endProp already exists in the registered end rule for this effect,
	 * then it replaces the original value, otherwise it is added as a new
	 * property.
	 * 
	 * @param end
	 *            New CSS Rule for end of the effect.
	 */
	public void setNewStartProperty(Property startProp) {
		registerProperties(theStart, theEnd, startProp, null);
		if (!effectElements.isEmpty())
			setUpEffect();
	}

	/**
	 * Set the new start style for the effect, and if the effectElement is not
	 * null, set up the effect again.
	 * 
	 * @param start
	 *            New CSS Rule for start of the effect.
	 */
	public void setNewStartStyle(Rule start) {
		registerProperties(start, theEnd, null, null);
		theStart = start;
		if (!effectElements.isEmpty())
			setUpEffect();
	}

	/**
	 * Set the new start and end styles for the effect, and if the effectElement
	 * is not null, set up the effect again.
	 * 
	 * @param start
	 *            New CSS Rule for start of the effect.
	 * @param end
	 *            New CSS Rule for end of the effect.
	 */
	public void setNewStyles(Rule start, Rule end) {
		registerProperties(start, end, null, null);
		theStart = start;
		theEnd = end;
		if (!effectElements.isEmpty())
			setUpEffect();
	}

	/**
	 * Set the position at which any determined binary effects will switch
	 * style.
	 * 
	 * @param switchFrameNumber
	 */
	public void setSwitchFrameNumber(double switchFrameNumber) {
		this.switchFrameNumber = switchFrameNumber;
	}

	/**
	 * Sets up the morph effect. Take the previously calculated set of property
	 * pairs and for each one ensure there are valid start and end values (which
	 * might involve calculating the start value from the attached element -
	 * which is why this step is done in this method rather than in the
	 * constructor.
	 * 
	 * With start and end values found, determine and register the appropriate
	 * Change object to use (ie scalar, binary, color, clip) to inact the morph
	 * for this property.
	 * 
	 */
	@Override
	public void setUpEffect() {
		// return if we have already done this (identifiable if property pairs
		// is
		// null)
		if (propertyPairs == null)
			return;

		registerEffectElement();
		
		elementEffects = new HashMap<Element, HashMap<String, ChangeInterface>>();
				
	    for (Iterator<Element> iter = effectElements.iterator(); iter.hasNext();){
	        Element effectElement = iter.next();
			HashMap<String, ChangeInterface> determinedEffects = new HashMap<String, ChangeInterface>();

			// Iterate over the property pairs to ensure values are set.
			for (Iterator<PropertyPair> it = propertyPairs.iterator(); it
					.hasNext();) {
				try {
					PropertyPair theMorph = it.next();
					String styleName = theMorph.end.getName();
					Property start = theMorph.start;
					Property end = theMorph.end;
					if (start == null) {
						// Attempt to get value from the DOM
						start = new Property(styleName
								+ ":"
								+ StyleImplementation.getComputedStyle(
										effectElement, styleName));
					}
					// Now determine that type of change that is required.
					ChangeInterface theChange;
					if (StyleImplementation.isColour(start.toString())) {
						// It's a colour change
						theChange = new NChangeColorAction(start.getValue(),
								end.getValue());
						determinedEffects.put(styleName, theChange);
						theChange.setUp(effectElement, styleName,
								switchFrameNumber);
					} else if (StyleImplementation.isClip(start.toString())) {
						// It's a clip change
						// Order of check moved due to Issue #108
						theChange = new NChangeClipAction(start
								.getUnitizedValue(), end.getUnitizedValue());
						determinedEffects.put(styleName, theChange);
						theChange.setUp(effectElement, styleName,
								switchFrameNumber);
					} else if (StyleImplementation.isScalar(start.toString(),
							start.getUnitizedValue())) {
						// It's a scalar change
						theChange = new NChangeScalarAction(start.getValue()
								+ start.getUnits(), end.getValue()
								+ end.getUnits());
						determinedEffects.put(styleName, theChange);
						theChange.setUp(effectElement, styleName,
								switchFrameNumber);
					} else {
						// Assume its a binary style and tell people
						GWT
						.log(
								"GWT-FX: Assuming that "
										+ start.getName()
										+ " is a style attribute that switches as a binary (have you forgotten to include the unit (px/pt/em etc)?",
								null);
						theChange = new NChangeBinaryAction(start.getValue(),
								end.getValue());
						determinedEffects.put(styleName, theChange);
						theChange.setUp(effectElement, styleName,
								switchFrameNumber);
					}
				} catch (RuntimeException e) {
					GWT.log("MorphEffect CSS Style Exception", e);
				}
			}
			elementEffects.put(effectElement, determinedEffects);
		}
		// Finally call reset() which applies the start style to the element.
		reset();
		// and we can get rid of the property pairs element since it has now
		// done
		// its job.
		propertyPairs = null;

		// Assign the start style to the element
		// CAUSED AN ERROR WITH IE (surprisingly) where the
		// getElementAttribute() method in GWT seems to not
		// return back a String.
		//
		// Would appear to be fixed in GWT 1.5 (release) :
		// http://groups.google.com/group/Google-Web-Toolkit-Contributors/browse_thread/thread/c708f46d592bcad8#
		//
		// if(GWT.isScript()){
		// DOM.setElementAttribute((com.google.gwt.user.client.Element)effectElement,
		// "style", DOM.getElementAttribute(
		// (com.google.gwt.user.client.Element)effectElement, "style") +
		// theStart.getDefinition());
		// }
		//    

	}

	@Override
	public void tearDownEffect(Element el) {
		HashMap<String, ChangeInterface> determinedEffects = elementEffects.get(el);
		for (Iterator<String> it = determinedEffects.keySet().iterator(); it.hasNext();) {
			ChangeInterface currChange = determinedEffects.get(it.next());
			currChange.tearDownEffect(el);
		}
	}
	
	
	public void tearDownEffect(){
		super.tearDownEffect();
		propertyPairs = null;
		theStart = null;
		theEnd = null;
	}

	public String toString(Element e) {
		String ret = "";
		HashMap<String, ChangeInterface> determinedEffects = elementEffects.get(e);
		for (Iterator<String> it = determinedEffects.keySet().iterator(); it
				.hasNext();) {
			String theChange = it.next();
			ChangeInterface currChange = determinedEffects.get(theChange);
			ret += currChange.toString() + "; ";
		}
		return ret;
	}

	public String toString() {
		StringBuffer sb = new StringBuffer();
		for (Iterator<Element> iter = effectElements.iterator(); iter.hasNext();) {
			Element e = iter.next();
			sb.append(e);
		}
		return sb.toString();
	}
}
